# PDF阅读器GUI程序
# 使用PySide6作为GUI框架，PyPDF2处理PDF文件

import sys
import os  # 添加os模块
from collections import OrderedDict

from PySide6.QtWidgets import (QApplication, QMainWindow, QFileDialog,
                              QVBoxLayout, QHBoxLayout, QWidget, QLabel, QSpinBox, 
                              QScrollArea, QLineEdit, QCheckBox, QPushButton,
                              QInputDialog)  # 添加 QInputDialog
from PySide6.QtCore import Qt, QSettings, QRectF
from PySide6.QtGui import QPixmap, QImage, QColor, QTransform
try:
    import fitz
except ImportError:
    print("缺少PyMuPDF依赖库，请通过以下命令安装：\npip install PyMuPDF")
    sys.exit(1)

class PDFReader(QMainWindow):
    def __init__(self):
        super().__init__()
        # 加载设置
        self.settings = QSettings('PDFReader', 'Settings')
        self.last_zoom = self.settings.value('zoom', 100, type=int)
        self.fit_mode = 'width'  # 添加缩放模式标记：'width' 或 'page'
        self.recent_files = self.settings.value('recent_files', [], type=list)
        self.max_recent_files = 5
        
        # 恢复上次的窗口位置和大小
        geometry = self.settings.value('geometry')
        if geometry:
            self.restoreGeometry(geometry)
        else:
            self.setGeometry(100, 100, 1000, 1200)
            
        self.setWindowTitle("PDF阅读器")
        self.pdf_document = None
        self.doc_path = None  # 添加文档路径属性
        self.current_page = 0  # 修改初始页码为0
        self.total_pages = 0
        self.original_qimg = None    
        self.original_pixmap = None  
        self.search_text = ""        # 搜索文本
        self.search_results = []     # 搜索结果列表
        self.current_result = -1     # 当前查找位置
        self.highlight_rect = None  # 添加高亮矩形属性
        self.last_search_text = ""   # 添加上次搜索文本记录
        self.current_result_page = 0  # 添加当前结果所在页码记录
        self.page_cache = OrderedDict()  # 使用OrderedDict替换普通dict
        self.max_cache_size = 10  # 最大缓存页数
        
        self.setAcceptDrops(True)
        
        self.init_ui()
        
        # 初始时禁用导航按钮
        self.update_navigation_buttons()

        # 恢复上次打开的文件
        last_file = self.settings.value('last_file', '', type=str)
        if last_file and os.path.exists(last_file):
            self.load_pdf(last_file)

    def __del__(self):
        """确保资源正确释放"""
        try:
            self.cleanup_document()
        except Exception:
            pass

    def cleanup_document(self):
        """优化的资源清理"""
        try:
            # 清空缓存
            if hasattr(self, 'page_cache'):
                self.page_cache.clear()
            
            # 清理图像资源
            if hasattr(self, 'original_qimg'):
                self.original_qimg = None
            if hasattr(self, 'original_pixmap'):
                self.original_pixmap = None
            
            # 清理文档
            if hasattr(self, 'pdf_document') and self.pdf_document:
                try:
                    self.pdf_document.close()
                except:
                    pass
                self.pdf_document = None
            
            # 重置状态
            self.doc_path = None
            self.current_page = 0  # 重置为0
            self.total_pages = 0
            
            # 更新导航按钮状态
            self.update_navigation_buttons()
            
            # 清理界面
            if hasattr(self, 'page_label'):
                self.page_label.clear()
            
            # 更新页码显示
            if hasattr(self, 'page_spin'):
                self.page_spin.setValue(0)
                self.page_spin.setMaximum(0)
            if hasattr(self, 'total_pages_label'):
                self.total_pages_label.setText(" / 0")
            
            # 更新按钮状态
            self.update_navigation_buttons()
            
            # 强制垃圾回收
            import gc
            gc.collect()
        
        except Exception as e:
            print(f"清理资源失败: {str(e)}")

    def open_pdf(self):
        """打开PDF文件"""
        try:
            file_path, _ = QFileDialog.getOpenFileName(
                self, "打开PDF文件", "", "PDF文件 (*.pdf)")
            if not file_path:
                return
                
            self.load_pdf(file_path)
                
        except Exception as e:
            self.show_error(f"打开PDF失败: {str(e)}")
            print(f"打开文档错误详情: {str(e)}")
            self.cleanup_document()

    def load_pdf(self, file_path):
        """加载PDF文件"""
        try:
            # 保存当前文件路径
            self.settings.setValue('last_file', file_path)
            
            # 更新最近文件列表
            self.update_recent_files(file_path)
            
            # 先完全清理之前的文档资源
            self.cleanup_document()
            
            try:
                # 打开并验证新文档
                self.pdf_document = fitz.open(file_path)
                if not self.pdf_document.is_pdf:  # 验证是否为有效的PDF
                    raise RuntimeError("不是有效的PDF文件")
                    
                if self.pdf_document.is_encrypted:  # 检查是否加密
                    # 尝试空密码
                    if not self.pdf_document.authenticate(""):
                        # 弹出密码输入对话框
                        password, ok = QInputDialog.getText(
                            self, 
                            "PDF密码", 
                            "请输入PDF文件密码:",
                            QLineEdit.Password
                        )
                        if ok and password:
                            if not self.pdf_document.authenticate(password):
                                raise RuntimeError("密码错误")
                        else:
                            raise RuntimeError("需要密码才能打开文件")
                
                # 更新文档信息
                self.doc_path = file_path
                self.total_pages = self.pdf_document.page_count
                print(f"成功打开文档，总页数: {self.total_pages}")
                
                # 更新UI状态
                self.page_spin.blockSignals(True)
                self.page_spin.setMaximum(self.total_pages)
                self.page_spin.setValue(1)
                self.page_spin.blockSignals(False)
                
                self.total_pages_label.setText(f" / {self.total_pages}")
                self.current_page = 1
                
                # 清空缓存
                self.page_cache.clear()
                
                # 更新导航按钮状态
                self.update_navigation_buttons()
                
                # 显示第一页
                self.show_page(1)
                
            except Exception as e:
                self.cleanup_document()
                raise RuntimeError(f"打开PDF失败: {str(e)}")
                
        except Exception as e:
            self.show_error(f"打开PDF失败: {str(e)}")
            print(f"打开文档错误详情: {str(e)}")
            self.cleanup_document()

    def show_page(self, page_num):
        """显示指定页码"""
        if not self.pdf_document:
            return
            
        try:
            # 确保页码有效
            page_num = max(1, min(page_num, self.total_pages))
            self.current_page = page_num
            
            # 尝试从缓存获取
            cached_pixmap = self.get_page_from_cache(page_num)
            if cached_pixmap:
                self.original_pixmap = cached_pixmap
                self.original_qimg = self.original_pixmap.toImage()
            else:
                # 获取并渲染页面
                page = self.pdf_document[page_num - 1]
                zoom = min(2.0, 1920 / page.rect.width)
                pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
                
                # 转换为Qt图像并显示
                img_data = pix.samples
                self.original_qimg = QImage(img_data, pix.width, pix.height, 
                                          pix.stride, QImage.Format_RGB888)
                if self.original_qimg.isNull():
                    raise RuntimeError("创建图像失败")
                    
                self.original_pixmap = QPixmap.fromImage(self.original_qimg)
                if self.original_pixmap.isNull():
                    raise RuntimeError("创建pixmap失败")
                
                # 缓存当前页面
                self.cache_page(page_num, self.original_pixmap)
            
            print(f"正在显示第 {page_num} 页") 
            
            # 更新页码输入框和显示
            self.page_spin.blockSignals(True)
            self.page_spin.setValue(page_num)
            self.page_spin.blockSignals(False)
            
            # 直接设置原始图像
            self.page_label.setPixmap(self.original_pixmap)
            
            # 应用缩放并显示
            if self.fit_button.isChecked():
                self.apply_fit_mode()
            else:
                self._apply_zoom(self.last_zoom)
            
            # 更新按钮状态
            self.update_navigation_buttons()
            
            # 确保更新显示
            self.page_label.update()
            self.content_widget.update()
            
            print(f"页面 {page_num} 显示完成")
            
            # 预加载相邻页面
            self.preload_adjacent_pages()
            
        except Exception as e:
            self.show_error(f"显示页面失败: {str(e)}")
            import traceback
            print(f"错误详情:\n{traceback.format_exc()}")

        # 触发预加载
        self.preload_adjacent_pages()

    def update_navigation_buttons(self):
        """更新导航按钮状态"""
        if not self.pdf_document:
            self.prev_button.setEnabled(False)
            self.next_button.setEnabled(False)
            return
            
        self.prev_button.setEnabled(self.current_page > 1)
        self.next_button.setEnabled(self.current_page < self.total_pages)

    def zoom_content(self, zoom_value):
        """缩放内容"""
        if not hasattr(self, 'original_pixmap') or self.original_pixmap.isNull():
            return
        self.last_zoom = zoom_value  # 保存当前缩放比例
        self._apply_zoom(zoom_value)

    def _apply_zoom(self, zoom_value):
        """应用缩放，使用QTransform提升性能"""
        try:
            if not self.original_pixmap or self.original_pixmap.isNull():
                return
                
            # 使用矩阵变换代替scaled方法
            transform = QTransform()
            scale_factor = zoom_value / 100.0
            transform.scale(scale_factor, scale_factor)
            
            scaled_pixmap = self.original_pixmap.transformed(
                transform,
                Qt.SmoothTransformation
            )
            
            # 更新显示
            if hasattr(self, 'page_label'):
                self.page_label.setPixmap(scaled_pixmap)
                # 优化最小尺寸设置
                min_size = max(1, int(self.original_pixmap.width() * scale_factor))
                self.page_label.setMinimumWidth(min_size)
                
        except Exception as e:
            self.show_error(f"缩放失败: {str(e)}")
            print(f"缩放错误详情: {str(e)}")

    def set_buttons_enabled(self, enabled):
        """设置按钮状态"""
        self.prev_button.setEnabled(enabled)
        self.next_button.setEnabled(enabled)

    def show_error(self, message):
        """统一显示错误信息"""
        try:
            if hasattr(self, 'page_label'):
                self.page_label.setText(f"[错误] {message}")  # 使用 page_label 显示错误信息
            else:
                print(f"[错误] {message}")  # 如果界面还未初始化，打印到控制台
        except Exception as e:
            print(f"显示错误信息失败: {str(e)}")
        finally:
            self.set_buttons_enabled(True)

    def prev_page(self):
        """显示上一页"""
        if not self.pdf_document:
            return
        
        if self.current_page > 1:
            try:
                new_page = self.current_page - 1
                self.show_page(new_page)
                # 更新导航按钮状态
                self.update_navigation_buttons()
            except Exception as e:
                self.show_error(f"切换到上一页失败: {str(e)}")
                import traceback
                print(f"错误详情:\n{traceback.format_exc()}")

    def next_page(self):
        """显示下一页"""
        if not self.pdf_document:
            return
        
        if self.current_page < self.total_pages:
            try:
                new_page = self.current_page + 1
                self.show_page(new_page)
                # 更新导航按钮状态
                self.update_navigation_buttons()
            except Exception as e:
                self.show_error(f"切换到下一页失败: {str(e)}")
                import traceback
                print(f"错误详情:\n{traceback.format_exc()}")

    def init_ui(self):
        """初始化UI组件"""
        # 设置全局字体
        try:
            font = self.font()
            font.setFamily("Microsoft YaHei")  # 使用微软雅黑
            self.setFont(font)
        except Exception as e:
            print(f"字体设置失败: {str(e)}")

        # 主部件和布局
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)
        self.main_layout = QVBoxLayout(self.central_widget)
        self.main_layout.setContentsMargins(10, 10, 10, 10)  # 设置边距

        # 顶部工具栏
        self.create_toolbar()
        
        # 内容区域
        self.create_content_area()
        
        # 底部导航栏
        self.create_navigation_bar()

    def create_toolbar(self):
        """创建工具栏"""
        self.toolbar_layout = QHBoxLayout()
        
        # 文件操作按钮
        self.open_button = QPushButton("打开PDF")
        self.open_button.setMinimumWidth(100)
        self.open_button.clicked.connect(self.open_pdf)
        self.toolbar_layout.addWidget(self.open_button)
        
        self.toolbar_layout.addSpacing(20)
        
        # 缩放控制
        self.zoom_label = QLabel("缩放:")
        self.toolbar_layout.addWidget(self.zoom_label)
        
        # 添加适应按钮
        self.fit_button = QPushButton("适应宽度")
        self.fit_button.setCheckable(True)  # 使按钮可以切换状态
        self.fit_button.setChecked(True)    # 默认选中
        self.fit_button.clicked.connect(self.toggle_fit_mode)
        self.toolbar_layout.addWidget(self.fit_button)
        
        self.zoom_spin = QSpinBox()
        self.zoom_spin.setRange(25, 400)
        self.zoom_spin.setSuffix("%")
        self.zoom_spin.setValue(self.last_zoom)  # 使用保存的缩放值
        self.zoom_spin.setSingleStep(25)
        self.zoom_spin.valueChanged.connect(self.zoom_content)
        self.toolbar_layout.addWidget(self.zoom_spin)
        
        self.toolbar_layout.addSpacing(20)
        
        # 页面导航
        self.page_label = QLabel("页码:")
        self.toolbar_layout.addWidget(self.page_label)
        
        self.page_spin = QSpinBox()
        self.page_spin.setMinimum(0)  # 修改最小值为0
        self.page_spin.setMaximum(0)  # 初始最大值为0
        self.page_spin.setValue(0)    # 初始值为0
        self.page_spin.valueChanged.connect(self.show_page)
        self.toolbar_layout.addWidget(self.page_spin)
        
        self.total_pages_label = QLabel(" / 0")  # 初始显示为0
        self.toolbar_layout.addWidget(self.total_pages_label)
        
        self.toolbar_layout.addStretch()
        self.main_layout.addLayout(self.toolbar_layout)

    def create_content_area(self):
        """创建内容显示区域"""
        # 创建滚动区域
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        
        # 内容容器
        self.content_widget = QWidget()
        self.content_layout = QVBoxLayout(self.content_widget)
        self.content_layout.setSpacing(0)  # 移除间距
        self.content_layout.setContentsMargins(0, 0, 0, 0)
        
        # 页面显示标签
        self.page_label = QLabel()
        self.page_label.setAlignment(Qt.AlignCenter)
        self.content_layout.addWidget(self.page_label)
        
        self.scroll_area.setWidget(self.content_widget)
        self.main_layout.addWidget(self.scroll_area)

    def create_navigation_bar(self):
        """创建底部导航栏"""
        self.nav_layout = QHBoxLayout()
        self.nav_layout.setContentsMargins(0, 5, 0, 5)
        
        # 添加弹簧
        self.nav_layout.addStretch()
        
        # 导航按钮
        self.prev_button = QPushButton("◀ 上一页")
        self.prev_button.setFixedWidth(100)
        self.prev_button.clicked.connect(self.prev_page)
        self.nav_layout.addWidget(self.prev_button)
        
        self.next_button = QPushButton("下一页 ▶")
        self.next_button.setFixedWidth(100)
        self.next_button.clicked.connect(self.next_page)
        self.nav_layout.addWidget(self.next_button)
        
        # 添加弹簧
        self.nav_layout.addStretch()
        
        self.main_layout.addLayout(self.nav_layout)

    def resizeEvent(self, event):
        """窗口大小改变时重新调整内容"""
        super().resizeEvent(event)
        # 只有加载了PDF文件且按钮存在并被选中时才应用适应模式
        if (hasattr(self, 'original_pixmap') and 
            self.original_pixmap is not None and 
            hasattr(self, 'fit_button') and 
            self.fit_button.isChecked()):
            self.apply_fit_mode()

    def closeEvent(self, event):
        """窗口关闭时保存设置"""
        # 保存窗口几何信息
        self.settings.setValue('geometry', self.saveGeometry())
        # 保存缩放比例
        self.settings.setValue('zoom', self.last_zoom)
        event.accept()

    def toggle_fit_mode(self):
        """切换适应模式"""
        if self.fit_mode == 'width':
            self.fit_mode = 'page'
            self.fit_button.setText("适应页面")
        else:
            self.fit_mode = 'width'
            self.fit_button.setText("适应宽度")
        
        # 应用新的缩放模式
        self.apply_fit_mode()

    def apply_fit_mode(self):
        """应用当前的适应模式"""
        if (not hasattr(self, 'original_pixmap') or 
            self.original_pixmap is None or 
            not hasattr(self, 'scroll_area')):
            return
            
        # 获取滚动区域的可见尺寸
        viewport_width = max(self.scroll_area.viewport().width() - 20, 1)  # 确保不为0
        viewport_height = max(self.scroll_area.viewport().height() - 20, 1)
        
        # 获取原始图像尺寸
        orig_width = self.original_pixmap.width()
        orig_height = self.original_pixmap.height()
        
        if self.fit_mode == 'width':
            # 计算适应宽度的缩放比例
            scale_ratio = viewport_width / orig_width
        else:  # fit_mode == 'page'
            # 计算适应页面的缩放比例
            width_ratio = viewport_width / orig_width
            height_ratio = viewport_height / orig_height
            scale_ratio = min(width_ratio, height_ratio)
        
        # 计算并设置缩放百分比（确保至少为25%）
        zoom_value = max(int(scale_ratio * 100), 25)
        self.zoom_spin.setValue(zoom_value)  # 这会触发 zoom_content

    def get_page_from_cache(self, page_num):
        """从缓存获取页面"""
        if page_num in self.page_cache:
            print(f"从缓存读取第 {page_num} 页")
            return self.page_cache[page_num]
        return None

    def cache_page(self, page_num, pixmap):
        """使用LRU策略缓存页面"""
        try:
            if page_num in self.page_cache:
                # 移动到最新
                self.page_cache.pop(page_num)
                self.page_cache[page_num] = pixmap
                return
                
            if len(self.page_cache) >= self.max_cache_size:
                # 删除最早的缓存
                self.page_cache.popitem(last=False)
                
            self.page_cache[page_num] = pixmap
            print(f"页面 {page_num} 已缓存")
            
        except Exception as e:
            print(f"缓存页面失败: {str(e)}")

    def preload_adjacent_pages(self):
        """智能预加载相邻页面"""
        if not self.pdf_document:
            return
            
        try:
            pages_to_load = []
            # 根据当前页码决定预加载顺序
            if self.current_page < self.total_pages:
                pages_to_load.append(self.current_page + 1)
            if self.current_page > 1:
                pages_to_load.append(self.current_page - 1)
                
            for page_num in pages_to_load:
                if page_num not in self.page_cache:
                    print(f"预加载第 {page_num} 页")
                    page = self.pdf_document[page_num - 1]
                    zoom = min(2.0, 1920 / page.rect.width)
                    pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
                    
                    # 使用with语句自动管理资源
                    img_data = pix.samples
                    qimg = QImage(img_data, pix.width, pix.height, 
                                pix.stride, QImage.Format_RGB888)
                    if not qimg.isNull():
                        pixmap = QPixmap.fromImage(qimg)
                        if not pixmap.isNull():
                            self.cache_page(page_num, pixmap)
                        
        except Exception as e:
            print(f"预加载失败: {str(e)}")

    def update_recent_files(self, file_path):
        """更新最近打开文件列表"""
        if file_path in self.recent_files:
            self.recent_files.remove(file_path)
        self.recent_files.insert(0, file_path)
        
        # 保持列表在最大长度以内
        self.recent_files = self.recent_files[:self.max_recent_files]
        self.settings.setValue('recent_files', self.recent_files)

    def dragEnterEvent(self, event):
        """处理拖入事件"""
        if event.mimeData().hasUrls():
            url = event.mimeData().urls()[0]
            if url.toLocalFile().lower().endswith('.pdf'):
                event.acceptProposedAction()

    def dropEvent(self, event):
        """处理放下事件"""
        file_path = event.mimeData().urls()[0].toLocalFile()
        if file_path.lower().endswith('.pdf'):
            self.load_pdf(file_path)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    # 设置默认字体为系统可用字体
    font = app.font()
    font.setFamily("Arial")  # 使用广泛支持的Arial字体
    app.setFont(font)
    reader = PDFReader()
    reader.show()
    sys.exit(app.exec())